// @ts-nocheck import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' import { isEqual } from 'lodash' import { AlertCircle, Book, Check } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useId, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { AiIconAnimation, Button, cn, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Form, FormControl, FormField, FormItem, Input, Label, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import * as z from 'zod' import { EDGE_FUNCTION_TEMPLATES } from '@/components/interfaces/Functions/Functions.templates' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import EdgeFunctionsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges' import { FileExplorerAndEditor } from '@/components/ui/FileExplorerAndEditor' import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types' import { useEdgeFunctionDeployMutation } from '@/data/edge-functions/edge-functions-deploy-mutation' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { BASE_PATH } from '@/lib/constants' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' // Array of adjectives and nouns for random function name generation const ADJECTIVES = [ 'quick', 'clever', 'bright', 'swift', 'rapid', 'smart', 'smooth', 'dynamic', 'super', 'hyper', ] const NOUNS = [ 'function', 'handler', 'processor', 'responder', 'worker', 'service', 'api', 'endpoint', 'action', 'task', ] // Function name validation regex - only allows alphanumeric characters, hyphens, and underscores const FUNCTION_NAME_REGEX = /^[A-Za-z0-9_-]+$/ // Define form schema with zod const FormSchema = z.object({ functionName: z .string() .min(1, 'Function name is required') .regex(FUNCTION_NAME_REGEX, 'Only letters, numbers, hyphens, and underscores allowed'), }) // Generate a random function name const generateRandomFunctionName = () => { const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)] const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)] return `${adjective}-${noun}` } // Convert invalid function name to valid one const sanitizeFunctionName = (name: string): string => { // Replace invalid characters with hyphens return name.replace(/[^A-Za-z0-9_-]/g, '-') } // Type for the form values type FormValues = z.infer const INITIAL_FILES: FileData[] = [ { id: 1, name: 'index.ts', content: EDGE_FUNCTION_TEMPLATES[0].content, state: 'new', }, ] const NewFunctionPage = () => { const router = useRouter() const { ref, template } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const snap = useAiAssistantStateSnapshot() const { mutate: sendEvent } = useSendEventMutation() const showStripeExample = useIsFeatureEnabled('edge_functions:show_stripe_example') const { openSidebar } = useSidebarManagerSnapshot() const [files, setFiles] = useState(INITIAL_FILES) const [selectedFileId, setSelectedFileId] = useState(INITIAL_FILES[0].id) const [open, setOpen] = useState(false) const templatesListboxId = useId() const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false) const [savedCode, setSavedCode] = useState('') const templates = useMemo(() => { if (showStripeExample) { return EDGE_FUNCTION_TEMPLATES } // Filter out Stripe template return EDGE_FUNCTION_TEMPLATES.filter((template) => template.value !== 'stripe-webhook') }, [showStripeExample]) const form = useForm({ resolver: zodResolver(FormSchema as any), defaultValues: { functionName: generateRandomFunctionName(), }, }) const { mutate: deployFunction, isPending: isDeploying, isSuccess: hasDeployed, } = useEdgeFunctionDeployMutation({ // [Joshen] To investigate: For some reason, the invalidation for list of edge functions isn't triggering onSuccess: () => { toast.success('Successfully deployed edge function') const functionName = form.getValues('functionName') // Allow the mutation state (isSuccess) to propagate before navigating // to prevent unnecessary dialog about unsaved changes setTimeout(() => { if (ref && functionName) { router.push(`/project/${ref}/functions/${functionName}/details`) } }, 150) }, }) const onSubmit = (values: FormValues) => { if (isDeploying || !ref) return deployFunction({ projectRef: ref, slug: values.functionName, metadata: { name: values.functionName, verify_jwt: true }, files: files.map(({ name, content }) => ({ name, content })), }) sendEvent({ action: 'edge_function_deploy_button_clicked', properties: { origin: 'functions_editor' }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } const handleChat = () => { const selectedFile = files.find((f) => f.id === selectedFileId) openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) snap.newChat({ name: 'Explain edge function', sqlSnippets: [selectedFile?.content ?? ''], initialInput: 'Help me understand and improve this edge function...', suggestions: { title: 'I can help you understand and improve your edge function. Here are a few example prompts to get you started:', prompts: [ { label: 'Explain Function', description: 'Explain what this function does...', }, { label: 'Optimize Function', description: 'Help me optimize this function...', }, { label: 'Add Features', description: 'Show me how to add more features...', }, { label: 'Error Handling', description: 'Help me handle errors better...', }, ], }, }) sendEvent({ action: 'edge_function_ai_assistant_button_clicked', properties: { origin: 'functions_editor_chat' }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } const onSelectTemplate = (templateValue: string) => { const template = EDGE_FUNCTION_TEMPLATES.find((t) => t.value === templateValue) if (template) { setFiles((prev) => prev.map((file) => file.id === selectedFileId ? { ...file, content: template.content } : file ) ) setOpen(false) sendEvent({ action: 'edge_function_template_clicked', properties: { templateName: template.name, origin: 'editor_page' }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } setIsPreviewingTemplate(false) } const handleTemplateMouseEnter = (content: string) => { if (!isPreviewingTemplate) { const selectedFile = files.find((f) => f.id === selectedFileId) ?? files[0] setSavedCode(selectedFile.content) } setIsPreviewingTemplate(true) setFiles((prev) => prev.map((f) => (f.id === selectedFileId ? { ...f, content } : f))) } const handleTemplateMouseLeave = () => { if (isPreviewingTemplate) { setIsPreviewingTemplate(false) setFiles((prev) => prev.map((f) => (f.id === selectedFileId ? { ...f, content: savedCode } : f)) ) } } // Try to sanitize function name when it's invalid const handleDeploy = () => { const currentName = form.getValues('functionName') const isValid = FUNCTION_NAME_REGEX.test(currentName) if (!isValid && currentName) { const sanitizedName = sanitizeFunctionName(currentName) form.setValue('functionName', sanitizedName, { shouldValidate: true }) } form.handleSubmit(onSubmit)() } useEffect(() => { if (template) { const templateMeta = EDGE_FUNCTION_TEMPLATES.find((x) => x.value === template) if (templateMeta) { form.reset({ functionName: template }) setSelectedFileId(1) setFiles([ { id: 1, name: 'index.ts', content: templateMeta.content, state: 'new', }, ]) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [template]) const hasUnsavedChanges = useMemo(() => !isEqual(INITIAL_FILES, files), [files]) return ( No templates found. {templates.map((template) => ( handleTemplateMouseEnter(template.content)} onMouseLeave={handleTemplateMouseLeave} className="cursor-pointer" >
f.content === template.content) ? 'opacity-100' : 'opacity-0' )} /> {template.name}
{template.description}
))}
} >
(
{form.formState.errors.functionName && ( {form.formState.errors.functionName.message} )}
)} />
) } NewFunctionPage.getLayout = (page: React.ReactNode) => { return ( {page} ) } export default NewFunctionPage